package gui;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.text.DecimalFormat;

import javax.swing.JPanel;

import logica.Mapa;
import menus.*;
import Aplicacion.Interfaz_grafica;
import Aplicacion.Variables_globales;

@SuppressWarnings("serial")
public class Lienzo extends JPanel implements Runnable {
	// Tama�o del Panel
	private static int PWIDTH = 500;
	private static int PHEIGHT = 400; 

	// Guarda el estado cada segundo (impreciso)
	private static long MAX_STATS_INTERVAL = 1000000000L;

	/* Numero de Frames con un retraso de 0 ms antes de que se una el proceso a otro */
	private static final int NO_DELAYS_PER_YIELD = 16;

	// Numero de frames que se pueden saltar en una animacion
	private static int MAX_FRAME_SKIPS = 5;

	// numero de FPS para calcular la media
	private static int NUM_FPS = 10;
	
	// Usado para Recoger Estadisticas
	private long	statsInterval		= 0L; // en ns
	private long	prevStatsTime;   
	private long	totalElapsedTime	= 0L;
	private long	gameStartTime;
	private int		timeSpentInGame		= 0; // en segundos

	private long	frameCount			= 0;
	private double	fpsStore[];
	private long	statsCount 			= 0;
	private double	averageFPS			= 0.0;
	
	private long	framesSkipped		= 0L;
	private long	totalFramesSkipped	= 0L;
	private double	upsStore[];
	private double	averageUPS			= 0.0;
	
	private DecimalFormat df = new DecimalFormat("0.##");		// 2 dp
	private DecimalFormat timedf = new DecimalFormat("0.####");	// 4 dp
	
	
	private Thread	animator;				// El proceso que realiza la animacion
	private boolean	running		= false;	// usado para detener el proceso de la animacion
	private boolean	isPaused	= false;
	
	private long period;					// periodo entre dibujado _nanosegundos_
	
	// usado para terminar el juego
	private boolean gameOver = false;
	private int score = 0;
	private Font font;
	private FontMetrics metrics;
	
	// renderizado
	private Graphics dbg; 
	private Image dbImage = null;
	
	// Variables del Juego
	private Ventana ventana;
	private final Mapa MAPA = new Mapa();
	private final MenuFooter footer = new MenuFooter();
	
	// Modos de juego
	private CustomButton mp;
	private PosicionarSoldados ps;
	private Atacar ao;
	private Defender dp;
	
	public Lienzo(Ventana ventana, long period) {
		this.ventana = ventana;
		this.period = period;
		
		this.setBackground(Color.WHITE);
		this.setPreferredSize(new Dimension(PWIDTH, PHEIGHT));
		
		super.setFocusable(true);
		super.requestFocus(); // ahora tiene el focus, recibira acciones de teclado
		this.readyForTermination();
		
		// observa los clicks de raton
		super.addMouseListener(new MouseListener() {
			public void mousePressed (MouseEvent e) {
				if (!Variables_globales.PAUSA) {
					MAPA.ratonPresionado(e.getX(),e.getY());
					footer.setBtnPress(true);
					footer.update(e.getX(), e.getY());
					if (mp != null) {
						mp.setBtnPress(true);
						mp.update(e.getX(), e.getY());
					}
					
					if (ps != null) {
						ps.setBtnPress(true);
						ps.update(e.getX(), e.getY());
					}
					
					if (ao != null) {
						ao.setBtnPress(true);
						ao.update(e.getX(), e.getY());
					}
					
					if (dp != null) {
						dp.setBtnPress(true);
						dp.update(e.getX(), e.getY());
					}
				}
			}

			@Override
			public void mouseClicked(MouseEvent e) {
				if (e.getX() <= 20 && e.getY() <= 20) {
					repaint();
				}
				
				if (!Variables_globales.PAUSA) {
					if (mp != null) {
						mp.setBtnClick(true);
						mp.update(e.getX(), e.getY());
					} else if (ps != null) {
						ps.setBtnClick(true);
						ps.update(e.getX(), e.getY());
					} else if (ao != null) {
						ao.setBtnClick(true);
						ao.update(e.getX(), e.getY());
					} else if (dp != null) {
						dp.setBtnClick(true);
						dp.update(e.getX(), e.getY());
					}
				}
			}

			@Override
			public void mouseEntered(MouseEvent e) {
				if (!Variables_globales.PAUSA) {
					if (mp != null) {
						mp.setBtnHover(true);
						mp.update(e.getX(), e.getY());
					} else if (ps != null) {
						ps.setBtnHover(true);
						ps.update(e.getX(), e.getY());
					} else if (ao != null) {
						ao.setBtnHover(true);
						ao.update(e.getX(), e.getY());
					} else if (dp != null) {
						dp.setBtnHover(true);
						dp.update(e.getX(), e.getY());
					}
				}
			}

			@Override
			public void mouseExited(MouseEvent e) {
				if (!Variables_globales.PAUSA) {
					if (mp != null) {
						mp.setBtnHover(false);
						mp.update(e.getX(), e.getY());
					} else if (ps != null) {
						ps.setBtnHover(false);
						ps.update(e.getX(), e.getY());
					} else if (ao != null) {
						ao.setBtnHover(false);
						ao.update(e.getX(), e.getY());
					} else if (dp != null) {
						dp.setBtnHover(false);
						dp.update(e.getX(), e.getY());
					}
				}
			}

			@Override
			public void mouseReleased(MouseEvent e) {
				if (!Variables_globales.PAUSA) {
					footer.setBtnPress(false);
					footer.update(e.getX(), e.getY());
					if (mp != null) {
						mp.setBtnPress(false);
						mp.setBtnClick(false);
					} else if (ps != null) {
						ps.setBtnPress(false);
						ps.update(e.getX(), e.getY());
					} else if (ao != null) {
						ao.setBtnPress(false);
						ao.update(e.getX(), e.getY());
					} else if (dp != null) {
						dp.setBtnPress(false);
						dp.update(e.getX(), e.getY());
					}
				}
			}
		});
		
		// Observa los cambios de tama�o del panel
		this.addComponentListener(new ComponentAdapter () {
			@Override
			public void componentResized(ComponentEvent e) {
				updateSize();
				MAPA.ventanaCambiada();
			}
		});
		
		// configura la fuente de los mensajes
		font = new Font("Ubuntu", Font.BOLD, 14);
		metrics = this.getFontMetrics(font);
		
		// Inicializa los elementos de tiempo
		fpsStore = new double[NUM_FPS]; // Registro de FPS
		upsStore = new double[NUM_FPS];	// Registro de UPS
		for (int i=0;i < NUM_FPS;i++) { // Se Inicializan a 0
			fpsStore[i] = 0.0;
			upsStore[i] = 0.0;
		}
	}
	
	private void updateSize () {
		PWIDTH = this.getWidth();
		PHEIGHT = this.getHeight();
		MAPA.setSize(PWIDTH, PHEIGHT);
		footer.setSize(PWIDTH, PHEIGHT);
		if (ps != null) ps.setSize(PWIDTH, PHEIGHT);
		
		// Se recrea la imagen con un nuevo tama�o
		dbImage = createImage(PWIDTH, PHEIGHT);
		if (dbImage == null) {
			System.out.println("dbImage is null");
			return;
		}
		else
			dbg = dbImage.getGraphics();
		
		gameRender();
	}
	
	/**
	 * Aqui se ejecutan las acciones/actualizaciones/eventos del juego
	 */
	public void gameUpdate() {
		if (!isPaused && !gameOver) {
			// AQUI VAN LAS ACCIONES DEL JUEGO EN EJECUCION
			
		}
	}
	
	/**
	 * Este m�todo se llama cada vez que se renderiza el Lienzo
	 */
	public void gameRender() {
		if (dbImage == null){
		      dbImage = createImage(PWIDTH, PHEIGHT);
		      if (dbImage == null) {
		        System.out.println("dbImage is null");
		        return;
		      }
		      else
		        dbg = dbImage.getGraphics();
		    }

		    // Limpia el Fondo
		    dbg.setColor(Color.white);
		    dbg.fillRect (0, 0, PWIDTH, PHEIGHT);

			// Aqu� se Dibujan los Elementos del Juego
			dbg = MAPA.llenarMapa(dbg);
			dbg = MAPA.paint(dbg);
			
			Graphics temporal = seleccionarModo(dbg);
			if (temporal != null) {
				dbg = temporal;
			}
			
			dbg.setColor(Color.WHITE);
		    dbg.setFont(font);

		    dbg = footer.paint(dbg);
		    
		    // DEBUG:
		    // Muestra un reporte de Estadisticas arriba a la izquierda
			dbg.drawString("Average FPS/UPS: " + df.format(averageFPS) + ", " +df.format(averageUPS), 12, 25);

			dbg.setColor(Color.black);

		    if (gameOver)
		      gameOverMessage(dbg);
	}
	
	/**
	 * Aqu� se pinta en pantalla los graficos
	 */
	public void paintScreen() {
		Graphics g;
	    try {
	      g = this.getGraphics();
	      if ((g != null) && (dbImage != null))
	        g.drawImage(dbImage, 0, 0, null);
	      g.dispose();
	      
	    } catch (Exception e) { 
	    	System.err.println("Error de Graficos, no se pudo cargar el canvas: " + e);
	    }
	}
	
/**
 * Eventos de la Aplicaci�n
 */
	
	/**
	 * Este M�todo se ejecuta cuando esta apunto de terminar/cerrar
	 */
	private void readyForTermination () {
		super.addKeyListener(new KeyAdapter () {
			// observa el teclado por esc, q, end, ctrl+c
			public void keyPressed (KeyEvent e) {
				int keyCode = e.getKeyCode();
				if ((keyCode == KeyEvent.VK_ESCAPE)	||
					(keyCode == KeyEvent.VK_Q)	||
					(keyCode == KeyEvent.VK_END)||
					((keyCode == KeyEvent.VK_C) && e.isControlDown())) {
					
					running = false;
				}
			}
		});
	}
	
	/**
	 * Este M�todo se llama al Iniciar el Juego
	 */
	public void startGame () {
		if (animator == null || !running) {
			animator = new Thread(this);
			animator.start();
		}
	}
	
	
	/**
	 * Este M�todo se llama al Resumir el Juego
	 */
	public void resumeGame() {
		isPaused = false;
	}
	
	/**
	 * Este M�todo se llama al Pausar el Juego
	 */
	public void pauseGame() {
		isPaused = true;
	}
	
	/**
	 * Este M�todo se llama al Detener el Juego
	 */
	public void stopGame () {
		running = false;
	}
	
/**
 * Timer Personalizado / Motor
 */
	
	@Override
	public void run() {
		long beforeTime, afterTime, timeDiff, sleepTime; // variables temporales (almacenan tiempo)
	    long overSleepTime = 0L; // tiempo que sobro en el ultimo sleep
	    int noDelays = 0; // numero de fps no ejecutados
	    long excess = 0L; // tiempo faltante

	    // Se recoge el tiempo inicial
	    gameStartTime = System.nanoTime();
	    prevStatsTime = gameStartTime;
	    beforeTime = gameStartTime;

		running = true; // se activa el indicador de ejecucion

		while(running) {
		  gameUpdate();
	      gameRender();
	      paintScreen();

	      afterTime = System.nanoTime();
	      timeDiff = afterTime - beforeTime;
//	      sleepTime = (period - timeDiff) - overSleepTime;  
	      sleepTime = (period - timeDiff);  
	      
//	      if (sleepTime > 0) { // Sobra tiempo?
	        try {
//	          Thread.sleep(sleepTime/1000000L);  // ns -> ms
	          Thread.sleep(5L);  // ns -> ms
	        }
	        catch(InterruptedException ex){
	        	this.repaint();
	        }
//	        overSleepTime = (System.nanoTime() - afterTime) - sleepTime;
//	      }
//	      else {    // sleepTime <= 0; el frame tardo demasiado en cargar
//	        excess -= sleepTime;  // guarda el tiempo de mas
//	        overSleepTime = 0L;
	        
//	        if (++noDelays >= NO_DELAYS_PER_YIELD) {
////	          Thread.yield();   // dale al proceso siguiente una oportunidad de cargar
//	          noDelays = 0;
//	        }
//	      }

	      beforeTime = System.nanoTime();

	      /* Si la animacion tarda demasiado en cargar, actualiza el estado del juego, pero no renderiza. */
	      int skips = 0;
	      while((excess > period) && (skips < MAX_FRAME_SKIPS)) {
	        excess -= period;
		    gameUpdate();    // Actualiza el estado pero no renderiza
	        skips++;
	      }
	      framesSkipped += skips;

	      storeStats();
		}

	    printStats();
	    System.exit(0); // la ventana desaparece
	}
	
/**
 * Printeo/Muestreo de Estadisticas
 */
	
	/**
	 * Mensaje de Fin de Juego
	 * @param g Graphics
	 */
	public void gameOverMessage(Graphics g) {}
	
	/**
	 * Se Almacena el Estado de las Estadisticas
	 */
	@SuppressWarnings("unused")
	public void storeStats() {
		frameCount++;
	    statsInterval += period;

	    if (statsInterval >= MAX_STATS_INTERVAL) {     // Guarda el Estado Cada MAX_STATS_INTERVAL
	      long timeNow = System.nanoTime();
	      timeSpentInGame = (int) ((timeNow - gameStartTime)/1000000000L);  // ns --> secs

	      long realElapsedTime = timeNow - prevStatsTime;   // Tiempo desde la ultima recolecci�n
	      totalElapsedTime += realElapsedTime;

	      double timingError = ((double)(realElapsedTime - statsInterval) / statsInterval) * 100.0;

	      totalFramesSkipped += framesSkipped;

	      double actualFPS = 0;     // Calcula los ultimos FPS y UPS
	      double actualUPS = 0;
	      if (totalElapsedTime > 0) {
	        actualFPS = (((double)frameCount / totalElapsedTime) * 1000000000L);
	        actualUPS = (((double)(frameCount + totalFramesSkipped) / totalElapsedTime) * 1000000000L);
	      }

	      // Guarda los ultimos FPS y UPS
	      fpsStore[ (int)statsCount%NUM_FPS ] = actualFPS;
	      upsStore[ (int)statsCount%NUM_FPS ] = actualUPS;
	      statsCount = statsCount+1;

	      double totalFPS = 0.0;     // numero de FPS guardados
	      double totalUPS = 0.0;
	      for (int i=0; i < NUM_FPS; i++) {
	        totalFPS += fpsStore[i];
	        totalUPS += upsStore[i];
	      }

	      if (statsCount < NUM_FPS) { // Se obtiene la media de FPS y UPS
	        averageFPS = totalFPS/statsCount;
	        averageUPS = totalUPS/statsCount;
	      }
	      else {
	        averageFPS = totalFPS/NUM_FPS;
	        averageUPS = totalUPS/NUM_FPS;
	      }
	      
	      framesSkipped = 0;
	      prevStatsTime = timeNow;
	      statsInterval = 0L;   // reset
	    }
	}
	
	/**
	 * Se printea las Estadisticas
	 */
	public void printStats() {
		System.out.println("Frame Count/Loss: " + frameCount + " / " + totalFramesSkipped);
		System.out.println("Average FPS: " + df.format(averageFPS));
		System.out.println("Average UPS: " + df.format(averageUPS));
	    System.out.println("Time Spent: " + timeSpentInGame + " secs");
	}
	
	/**
	 * 
	 * @param g
	 * @return
	 */
	
	public Graphics seleccionarModo(Graphics g)	{
		Graphics g_tmp = null;
		
		switch (Variables_globales.ESTADO)	{
			case MENU :
				ps = null;
				if (mp == null) mp = new MenuPrincipal();
				g_tmp = mp.paint(g);
				break;
				
			case POSICIONAR :
				mp = null;
				if (ps == null) {
					ps = new PosicionarSoldados();
					PWIDTH = this.getWidth();
					PHEIGHT = this.getHeight();
					ps.setSize(PWIDTH, PHEIGHT);
				}
				g_tmp = ps.paint(g);
				break;
				
			case ATACAR : 
				if (ao == null) ao = new Atacar();
				g_tmp = ao.paint(g);
				break;
				
			case DEFENDER :
				if (dp == null) dp = new Defender();
				g_tmp = dp.paint(g);
				break;
				
			case FIN_DEL_JUEGO :
				break;
				
			case PREFERENCIAS : 
				break;
		}
		
		return g_tmp;
	}
}
